#!/usr/bin/python2.7
# -*-  encoding: utf-8 -*-

import ctypes
import os
import shutil
import threading
import traceback
import commands

import addon_manager.addon_utils
from addon_manager.addon_exception import (
    AddOnInstallPackageUncompressFailed,
    AddOnInstallPackageNotExist,
    AddOnInstallPackageMoveFailed,
    InvalidAddonConfigFile,
    RunningAddOnOpException,
    AddOnInstallPackageBusyInstaller,
    AddOnInstallPackageNoInstallTask,
    AddOnInstallPackageCompressFailed,
    AddOnInstallPackageTimeout,
    AddOnInstallPackageWrongFormat,
    BuiltinAddOnOperateFailed,
    AddOnServiceNumConstraint,
)
from addon_manager.addon_logger import AddOnLogger
from addon_manager.addon_requirement_mgr import RequirementMgr
from addon_manager.addon_status import AddOnStatus
from addon_manager.addon_eventqueue import AddOnEventID, AddOnEventQueue
from addon_manager.addon_whitelist import AddOnWhitelist


def sync_path(path):
    if os.path.isfile(path):
        fd = os.open(path, os.O_RDWR)
        os.fsync(fd)
        os.close(fd)
        # AddOnLogger().debug("[addon] {} is synced to disk, size: {}".format(path, os.path.getsize(path)))
    elif os.path.isdir(path):
        file_list = os.listdir(path)
        for file_name in file_list:
            file_path = os.path.join(path, file_name)
            if os.path.islink(file_path):
                AddOnLogger().debug("[addon] file link {} is jumped.".format(file_path))
            elif os.path.isfile(file_path):
                fd = os.open(file_path, os.O_RDWR)
                os.fsync(fd)
                os.close(fd)
                # AddOnLogger().debug("[addon] {} is synced to disk, size: {}".format(file_path, os.path.getsize(file_path)))
            elif os.path.isdir(file_path):
                sync_path(file_path)
                # AddOnLogger().debug("[addon] subdir {} is synced to disk".format(file_path))


class FTPPathParser(object):
    """
    存放AddOn的安装信息
        e.g. '/usr/etc/jkzuc/scripts/read_io.tar.gz'
            ftppath: '/usr/etc/jkzuc/scripts/read_io.tar.gz'
            name_with_suffix: read_io.tar.gz
            name: read_io
            suffix: .tar.gz
    """

    def __init__(self, ftpdir, config):
        self.ftppath = ftpdir
        self.config = config
        self.__parse()

    def __parse(self):
        self.name_with_suffix = os.path.basename(self.ftppath)

        if not self.name_with_suffix.endswith(".tar.gz"):
            raise AddOnInstallPackageUncompressFailed(self.ftppath)

        self.suffix = ".tar.gz"
        cmds = "tar -ztf {}".format(self.ftppath)
        status, output = commands.getstatusoutput(cmds)
        if status != 0:
            raise AddOnInstallPackageUncompressFailed(self.ftppath)

        AddOnLogger().debug("output is:")
        AddOnLogger().debug(output)
        # self.name, self.suffix, _ = self.name_with_suffix.rpartition('.tar.gz')


class AddOnInstaller(object):
    """
    声明AddOnInstaller需要支持的接口
    """

    _RET_TIMEOUT = -1
    _RET_SUCC = 0
    _RET_FAIL = 1

    @staticmethod
    def command_with_timeout(cmd, timeout=0):
        """
        @param cmd 执行shell命令
        @param timeout 超时时间
        """
        import sys
        import time
        import subprocess

        pipe = subprocess.Popen(args=cmd, stdout=sys.stdout, stderr=sys.stderr)
        if timeout == 0:
            pipe.wait()
        else:
            time.sleep(timeout)

        ret_code = pipe.poll()  # None: 运行中
        if ret_code is None:
            pipe.terminate()
            return AddOnInstaller._RET_TIMEOUT
        elif ret_code == 0:
            AddOnLogger().debug("tar succ")
            return AddOnInstaller._RET_SUCC
        else:
            AddOnLogger().error("tar failed")
            return AddOnInstaller._RET_FAIL

    def __init__(self, config):
        self.config = config
        self.inst_full_path = ""
        self.inst_tar_path = ""
        self.addon_name = None

    def _mv_from_ftp_to_install(self, src_path):
        if not os.path.exists(src_path):
            raise AddOnInstallPackageNotExist()

        inst_dir = self.config.get_install_dir()
        if not os.path.exists(inst_dir):
            os.makedirs(inst_dir)

        mv_cmd = "mv {} {}".format(src_path, inst_dir)
        ret = os.system(mv_cmd)
        if ret != 0:
            raise AddOnInstallPackageMoveFailed(inst_dir)

    def _uncompress(self, inst_tar_path, addon_name, timeout=0):
        """
        @param tar_path 压缩包名称
        @param timeout 超时时间. 0 阻塞直到完成
        """
        inst_dir = self.config.get_install_dir()
        inst_full_path = os.path.join(inst_dir, addon_name)

        # --no-same-owner: 不以压缩时的权限作为当前的权限
        uncompress_cmd = ["tar", "zx", "--no-same-owner", "--no-same-permissions"]
        uncompress_cmd += ["-f", inst_tar_path]
        uncompress_cmd += ["-C", inst_dir]
        uncompress_cmd_str = " ".join(uncompress_cmd)
        AddOnLogger().debug(uncompress_cmd_str)

        ret = AddOnInstaller.command_with_timeout(uncompress_cmd, timeout=timeout)
        try:
            addon_manager.addon_utils.chdirmode(inst_full_path, 0o755)
        except Exception:
            ret = self._RET_FAIL

        # 错误处理
        if ret == self._RET_TIMEOUT:
            self._handle_uncompress_failed(inst_full_path)
            raise AddOnInstallPackageTimeout("{} timeout.".format(uncompress_cmd_str))
        elif ret == self._RET_FAIL:
            self._handle_uncompress_failed(inst_full_path)
            raise AddOnInstallPackageUncompressFailed(inst_tar_path)

    def _check_compress_pack_then_set_addonname(self, ftppath):
        # Addons目录下的压缩包路径
        inst_dir = self.config.get_install_dir()
        name_with_suffix = os.path.basename(ftppath)
        self.inst_tar_path = os.path.join(inst_dir, name_with_suffix)

        # 检查压缩包后缀
        if not name_with_suffix.endswith(".tar.gz"):
            raise AddOnInstallPackageUncompressFailed(ftppath)

        # 获取压缩包内文件列表
        cmds = "tar -ztf {}".format(self.inst_tar_path)
        status, output = commands.getstatusoutput(cmds)
        if status != 0:
            raise AddOnInstallPackageUncompressFailed(self.inst_tar_path)

        # 判断压缩包是否为空
        filename_list = output.split()
        if len(filename_list) < 1:
            err_msg = "[{}] empty package {}".format(name_with_suffix, filename_list)
            raise AddOnInstallPackageWrongFormat(err_msg)

        # 获取AddOn解压后的根目录名称
        addon_name = filename_list[0]
        while addon_name.startswith("./"):
            addon_name = addon_name[2:]

        while addon_name.endswith("/"):
            addon_name = addon_name[:-1]

        # 检查所有的AddOn文件是否在同一目录下
        for filename in filename_list[1:]:
            if filename_list[0] not in filename:
                err_msg = "[{}] file {} is not in dir {}".format(name_with_suffix, filename, filename_list[0])
                raise AddOnInstallPackageWrongFormat(err_msg)

        self.addon_name = addon_name

    def _handle_uncompress_failed(self, inst_full_path):
        try:
            shutil.rmtree(inst_full_path)
        except Exception:
            AddOnLogger().error(traceback.format_exc())

    def _compress(self, addon_name, timeout=0):
        """
        @param addon_name: addon名称
        @param timeout: 超时时间. 0 阻塞直到完成
        """
        exclude_dir = [".venv"]
        install_dir = self.config.get_install_dir()
        pack_full_path = "/usr/etc/jkzuc/scripts/{}.tar.gz".format(addon_name)

        compress_cmd = [
            "tar",
            "-zcv",
        ]
        for d in exclude_dir:
            compress_cmd += ["--exclude", d]
        compress_cmd.append("-f")
        compress_cmd.append(pack_full_path)
        compress_cmd += ["-C", install_dir]
        compress_cmd.append(addon_name)
        compress_cmd_str = " ".join(compress_cmd)

        ret = AddOnInstaller.command_with_timeout(compress_cmd, timeout=timeout)

        # 错误处理
        if ret == self._RET_TIMEOUT:
            self._handle_compress_failed(pack_full_path)
            raise AddOnInstallPackageTimeout("{} timeout.".format(compress_cmd_str))
        elif ret == self._RET_FAIL:
            self._handle_compress_failed(pack_full_path)
            raise AddOnInstallPackageCompressFailed(pack_full_path)

    def _handle_compress_failed(self, pack_full_path):
        try:
            os.remove(pack_full_path)
        except Exception:
            AddOnLogger().error(traceback.format_exc())

    def _try_install_requirement(self, inifile):
        """
        有依赖则安装
        """
        AddOnLogger().debug("RequirementMgr start")
        ret = RequirementMgr.StartImport(inifile)
        AddOnLogger().debug("RequirementMgr ret: %s", ret)
        if ret != RequirementMgr.SUCCESS:
            raise InvalidAddonConfigFile()

    def _del_install_pack(self, path):
        if os.path.exists(path):
            del_cmd = "rm -rf {}".format(path)
            AddOnLogger().debug(del_cmd)
            os.system(del_cmd)

    def on_install_async_finish(self):
        raise NotImplementedError()

    def _check_before_install(self, addon_name):
        whitelist = AddOnWhitelist.inst()
        if whitelist.is_update_disable(addon_name):
            raise BuiltinAddOnOperateFailed(addon_name, "installed")

        addon_stat_inst = AddOnStatus.inst()
        if addon_stat_inst.check_if_addon_is_running(addon_name):
            raise RunningAddOnOpException()

        if len(addon_stat_inst) >= self.config.addon_max_limit:
            raise AddOnServiceNumConstraint()

    def install(self, ftppath):
        raise NotImplementedError()

    def _check_before_uninstall(self, addon_name):
        whitelist = AddOnWhitelist.inst()
        if whitelist.is_delete_disable(addon_name):
            raise BuiltinAddOnOperateFailed(addon_name, "uninstalled")

    def uninstall(self, service_path):
        raise NotImplementedError()

    def _check_before_pack(self, addon_name):
        whitelist = AddOnWhitelist.inst()
        if whitelist.is_export_disable(addon_name):
            raise BuiltinAddOnOperateFailed(addon_name, "pack")

    def on_pack_async_finish(self):
        raise NotImplementedError()

    def pack_addon(self, addon_name):
        raise NotImplementedError()

    def get_install_status(self):
        raise NotImplementedError()


class AddOnAsyncInstaller(AddOnInstaller):
    """
    暂不支持并发安装和卸载的功能, AddOnInstaller异步安装实现
    由于AddOn的安装和导出共用了一些状态信息,所以不能同时执行install_async安装和pack_async
    """

    __IDLE = 0
    __INSTALL = 1
    __INSTALL_FIN = 2
    __UNINSTALL = 3
    __PACK = 4
    __PACKFIN = 5
    __INSTALL_STOP = 6
    __PACK_STOP = 7

    def __init__(self, config):
        """
        @param config AddOnConfig的实例
        """
        AddOnInstaller.__init__(self, config=config)
        self.__install_thread = None
        self.__install_status = self.__IDLE
        self.__install_ftppath = None
        self.__progress = 0  # 0 ~ 100
        self.__install_exception = None
        self.addon_name = None

    def __install_async(self):
        if self.__install_status != self.__INSTALL:
            return

        self.__install_exception = None
        ftppath = self.__install_ftppath
        self.inst_full_path = ""
        self.inst_tar_path = ftppath
        try:
            self.__progress = 0
            self._mv_from_ftp_to_install(ftppath)
            AddOnLogger().debug("mv_from_ftp_to_install %s successfully", ftppath)

            self._check_compress_pack_then_set_addonname(ftppath)
            self.__progress = 15

            # 安装前检查
            self._check_before_install(self.addon_name)

            sync_path(self.inst_tar_path)

            inst_dir = self.config.get_install_dir()
            self.inst_full_path = os.path.join(inst_dir, self.addon_name)
            self._uncompress(self.inst_tar_path, self.addon_name, timeout=0)
            self.__progress = 30

            sync_path(self.inst_full_path)
            self.__progress = 60

            addon_inifile_path = addon_manager.addon_utils.get_addon_ini_file_path(self.config, self.addon_name)
            self._try_install_requirement(addon_inifile_path)
            self.__progress = 95

        except Exception as e:
            AddOnLogger().error(traceback.format_exc())
            self.__install_exception = e
        finally:
            self._del_install_pack(self.inst_tar_path)
            self.inst_tar_path = ""
            AddOnEventQueue.inst().dispatch(AddOnEventID.EVENT_ON_INSTALL_FINISH)
            self.__install_status = self.__INSTALL_FIN

    def on_install_async_finish(self):
        if self.__install_status != self.__INSTALL_FIN:
            return

        addon_stat_inst = AddOnStatus.inst()
        try:
            if isinstance(self.__install_exception, Exception):
                raise self.__install_exception
            # addon_cfg_parser.fill(addon_info)
            # 安装完成后，尝试自动启动addon
            # self.executor.start_by_addoninfo(addon_info)
            addon_info = addon_stat_inst.regist(self.addon_name)
            AddOnLogger().info("addon_info str %s", addon_info)
            addon_stat_inst.set_can_reload_from_disk(True)

            self.__progress = 100

        except RunningAddOnOpException as e:
            AddOnLogger().error(traceback.format_exc())
            self.__install_exception = e

        except Exception as e:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("rm dir %s", self.inst_full_path)
            if os.path.exists(self.inst_full_path):
                shutil.rmtree(self.inst_full_path)
            addon_info = addon_stat_inst.unregist(self.addon_name)
            self.__install_exception = e
        finally:
            self.__install_thread = None
            self.__install_status = self.__INSTALL_STOP

    def install(self, ftppath):
        """
        安装AddOn
        @param ftppath
            1. 需要添加压缩后缀。便于以后支持多种压缩格式的AddOn上传
            2. 需要上传绝对路径。因为从FTP来源的AddOn安装包, AddOn无法决定安装包的上传路径
            e.g. /usr/etc/jkzuc/scripts/read_io.tar.gz
        """
        if self.__install_status != self.__IDLE:
            raise AddOnInstallPackageBusyInstaller(ftppath)

        addon_stat_inst = AddOnStatus.inst()
        addon_stat_inst.set_can_reload_from_disk(False)

        # 在子线程执行install
        self.__install_status = self.__INSTALL
        self.__install_ftppath = ftppath
        self.__install_exception = None
        self.__progress = 0
        self.__install_thread = threading.Thread(target=self.__install_async)
        self.__install_thread.start()

        return {}

    def uninstall(self, service_path):
        """
        删除AddOn
        """
        if self.__install_status != self.__IDLE:
            raise AddOnInstallPackageBusyInstaller(service_path)

        # 检查AddOn是否运行中
        addon_name = addon_manager.addon_utils.get_addonname_from_servicepath(service_path)
        self._check_before_uninstall(addon_name)

        addon_stat_inst = AddOnStatus.inst()
        if addon_stat_inst.check_if_addon_is_running(addon_name):
            raise RunningAddOnOpException()

        self.__install_status = self.__UNINSTALL

        try:
            addon_install_dir = addon_manager.addon_utils.get_addon_install_dir(self.config, addon_name)
            AddOnLogger().debug("delete addon (%s)", addon_install_dir)
            self._del_install_pack(addon_install_dir)
            addon_stat_inst = AddOnStatus.inst()
            addon_stat_inst.unregist(addon_name)
        finally:
            self.__install_status = self.__IDLE

    def __pack_async(self, addon_name):
        if self.__install_status != self.__PACK:
            return

        exclude_option = "--exclude"
        exclude_dir = [".venv"]
        exclude_str = ""
        for d in exclude_dir:
            exclude_str = exclude_option + " " + d + " "

        self.__progress = 5
        try:
            self._compress(addon_name)
        except Exception as e:
            AddOnLogger().error(traceback.format_exc())
            self.__install_exception = e
        finally:
            AddOnEventQueue.inst().dispatch(AddOnEventID.EVENT_ON_PACK_FINISH)
            self.__install_status = self.__PACKFIN
            self.__install_thread = None

    def on_pack_async_finish(self):
        if self.__install_status != self.__PACKFIN:
            return
        try:
            if isinstance(self.__install_exception, Exception):
                raise self.__install_exception
            self.__progress = 100
        finally:
            self.__install_status = self.__PACK_STOP

    def pack_addon(self, addon_name):
        """
        打包AddOn
        """
        if self.__install_status != self.__IDLE:
            raise AddOnInstallPackageBusyInstaller(addon_name)

        self._check_before_pack(addon_name)

        # 检查AddOn是否运行中
        addon_stat_inst = AddOnStatus.inst()
        if addon_stat_inst.check_if_addon_is_running(addon_name):
            raise RunningAddOnOpException()

        # 在子线程执行install
        self.__install_status = self.__PACK
        self.__install_exception = None
        self.__progress = 0
        self.__install_thread = threading.Thread(target=AddOnAsyncInstaller.__pack_async, args=(self, addon_name))
        self.__install_thread.start()

    def __is_finish(self):
        return self.__install_status == self.__INSTALL_STOP or self.__install_status == self.__PACK_STOP

    def __is_run(self):
        return self.__install_status in [self.__INSTALL, self.__INSTALL_FIN, self.__PACK, self.__PACKFIN]

    def get_install_status(self):
        if self.__progress == 100:
            self.__progress = 0
            self.__install_status = self.__IDLE  # 确保安装状态被获取后再切换到IDLE模式
            return {"progress": 100}

        if self.__install_exception and self.__is_finish():  # 只在完成状态下的参数清理，避免线程间的竞争
            e = self.__install_exception
            self.__install_exception = None
            self.__progress = 0
            self.__install_status = self.__IDLE  # 确保安装状态被获取后再切换到IDLE模式
            raise e

        if self.__is_run() or self.__is_finish():
            return {"progress": self.__progress}

        raise AddOnInstallPackageNoInstallTask()


class AddonSyncInstaller(AddOnInstaller):
    """
    暂不支持并发安装和卸载的功能, AddOnInstaller同步安装实现
    """

    __TIMEOUT_DURATION = 2.5  # 超时时间

    def __init__(self, config):
        """
        @param config AddOnConfig的实例
        """
        AddOnInstaller.__init__(self, config=config)

    def install(self, ftppath):
        """
        @param ftppath
            1. 需要添加压缩后缀。便于以后支持多种压缩格式的AddOn上传
            2. 需要上传绝对路径。因为从FTP来源的AddOn安装包, AddOn无法决定安装包的上传路径
            e.g. /usr/etc/jkzuc/scripts/read_io.tar.gz
        """
        # self.__check_filesize(ftppath)
        self.inst_tar_path = ftppath
        self._mv_from_ftp_to_install(ftppath)
        AddOnLogger().debug("mv_from_ftp_to_install %s successfully", ftppath)

        self._check_compress_pack_then_set_addonname(ftppath)
        self._check_before_install(self.addon_name)

        inst_dir = self.config.get_install_dir()
        inst_full_path = os.path.join(inst_dir, self.addon_name)

        sync_path(self.inst_tar_path)
        try:
            self._uncompress(self.inst_tar_path, self.addon_name, timeout=self.__TIMEOUT_DURATION)
        finally:
            self._del_install_pack(self.inst_tar_path)

        sync_path(inst_full_path)

        addon_stat_inst = AddOnStatus.inst()
        try:
            # addon_cfg_parser.fill(addon_info)
            addon_info = addon_stat_inst.regist(self.addon_name)
            AddOnLogger().info("addon_info str %s", addon_info)
            self._try_install_requirement(addon_info["inifile"])
            return addon_info["ini_conf"]
        except Exception as e:
            AddOnLogger().error(traceback.format_exc())
            AddOnLogger().error("rm dir %s", addon_info["basedir"])
            shutil.rmtree(addon_info["basedir"])
            addon_info = addon_stat_inst.unregist(self.addon_name)
            raise e
        finally:
            self._del_install_pack(self.inst_tar_path)

    def uninstall(self, service_path):
        addon_name = addon_manager.addon_utils.get_addonname_from_servicepath(service_path)
        self._check_before_uninstall(addon_name)

        addon_stat_inst = AddOnStatus.inst()
        if addon_stat_inst.check_if_addon_is_running(addon_name):
            raise RunningAddOnOpException()

        addon_install_dir = addon_manager.addon_utils.get_addon_install_dir(self.config, addon_name)
        AddOnLogger().debug("delete addon (%s)", addon_install_dir)
        self._del_install_pack(addon_install_dir)
        addon_stat_inst = AddOnStatus.inst()
        addon_stat_inst.unregist(addon_name)

    def pack_addon(self, filename):
        self._check_before_pack(filename)

        self._compress(filename, self.__TIMEOUT_DURATION)
        AddOnLogger().debug("tar %s ok !", filename)


__all__ = ["AddOnInstaller", "AddOnAsyncInstaller", "AddonSyncInstaller"]

if __name__ == "__main__":
    from addon_manager.addon_config import AddOnConfig

    test_ftp_path = "/usr/etc/jkzuc/scripts/io_event_triger.tar.gz"

    def test_FTP_path_parser():
        ftp_parser = FTPPathParser(test_ftp_path, AddOnConfig.inst())
        print("name_with_suffix", ftp_parser.name_with_suffix)
        print("name", ftp_parser.name)
        print("suffix", ftp_parser.suffix)

    # Test_FTPPathParser()

    def test_addon_installer():
        # installer = AddOnInstaller(AddOnConfig.inst)
        # installer.install(test_ftp_path)
        pass

    test_addon_installer()
